﻿using System;

namespace Grimgine.Math
{
    /// <summary>
    /// 
    /// </summary>
    public struct Matrix4 : IEquatable<Matrix4>
    {
        #region Fields

        public float M11, M12, M13, M14;
        public float M21, M22, M23, M24;
        public float M31, M32, M33, M34;
        public float M41, M42, M43, M44;

        #endregion

        #region Properties

        /// <summary>
        /// 
        /// </summary>
        public static Matrix4 Identity
        {
            get
            {
                return
                (
                    new Matrix4
                    (
                        1, 0, 0, 0,
                        0, 1, 0, 0,
                        0, 0, 1, 0,
                        0, 0, 0, 1
                    )
                );
            }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// 
        /// </summary>
        public Matrix4(float M11, float M12, float M13, float M14,
                              float M21, float M22, float M23, float M24,
                              float M31, float M32, float M33, float M34,
                              float M41, float M42, float M43, float M44)
        {
            this.M11 = M11; this.M12 = M12; this.M13 = M13; this.M14 = M14;
            this.M21 = M21; this.M22 = M22; this.M23 = M23; this.M24 = M24;
            this.M31 = M31; this.M32 = M32; this.M33 = M33; this.M34 = M34;
            this.M41 = M41; this.M42 = M42; this.M43 = M43; this.M44 = M44;
        }

        #endregion

        #region Matrix Arithmetic

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static Matrix4 operator +(Matrix4 a, Matrix4 b)
        {
            return (
                new Matrix4(
                    a.M11 + b.M11, a.M12 + b.M12, a.M13 + b.M13, a.M14 + b.M14,
                    a.M21 + b.M21, a.M22 + b.M22, a.M23 + b.M23, a.M24 + b.M24,
                    a.M31 + b.M31, a.M32 + b.M32, a.M33 + b.M33, a.M34 + b.M34,
                    a.M41 + b.M41, a.M42 + b.M42, a.M43 + b.M43, a.M44 + b.M44
                )
            );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static Matrix4 operator -(Matrix4 a, Matrix4 b)
        {
            return (
                new Matrix4(
                    a.M11 - b.M11, a.M12 - b.M12, a.M13 - b.M13, a.M14 - b.M14,
                    a.M21 - b.M21, a.M22 - b.M22, a.M23 - b.M23, a.M24 - b.M24,
                    a.M31 - b.M31, a.M32 - b.M32, a.M33 - b.M33, a.M34 - b.M34,
                    a.M41 - b.M41, a.M42 - b.M42, a.M43 - b.M43, a.M44 - b.M44
                )
            );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static Matrix4 operator *(Matrix4 a, Matrix4 b)
        {
            Matrix4 result;

            // First row
            result.M11 = a.M11 * b.M11 + a.M12 * b.M21 + a.M13 * b.M31 + a.M14 * b.M41;
            result.M12 = a.M11 * b.M12 + a.M12 * b.M22 + a.M13 * b.M32 + a.M14 * b.M42;
            result.M13 = a.M11 * b.M13 + a.M12 * b.M23 + a.M13 * b.M33 + a.M14 * b.M43;
            result.M14 = a.M11 * b.M14 + a.M12 * b.M24 + a.M13 * b.M34 + a.M14 * b.M44;

            // Second row
            result.M21 = a.M21 * b.M11 + a.M22 * b.M21 + a.M23 * b.M31 + a.M24 * b.M41;
            result.M22 = a.M21 * b.M12 + a.M22 * b.M22 + a.M23 * b.M32 + a.M24 * b.M42;
            result.M23 = a.M21 * b.M13 + a.M22 * b.M23 + a.M23 * b.M33 + a.M24 * b.M43;
            result.M24 = a.M21 * b.M14 + a.M22 * b.M24 + a.M23 * b.M34 + a.M24 * b.M44;

            // Third row
            result.M31 = a.M31 * b.M11 + a.M32 * b.M21 + a.M33 * b.M31 + a.M34 * b.M41;
            result.M32 = a.M31 * b.M12 + a.M32 * b.M22 + a.M33 * b.M32 + a.M34 * b.M42;
            result.M33 = a.M31 * b.M13 + a.M32 * b.M23 + a.M33 * b.M33 + a.M34 * b.M43;
            result.M34 = a.M31 * b.M14 + a.M32 * b.M24 + a.M33 * b.M34 + a.M34 * b.M44;

            // Fourth row
            result.M41 = a.M41 * b.M11 + a.M42 * b.M21 + a.M43 * b.M31 + a.M44 * b.M41;
            result.M42 = a.M41 * b.M12 + a.M42 * b.M22 + a.M43 * b.M32 + a.M44 * b.M42;
            result.M43 = a.M41 * b.M13 + a.M42 * b.M23 + a.M43 * b.M33 + a.M44 * b.M43;
            result.M44 = a.M41 * b.M14 + a.M42 * b.M24 + a.M43 * b.M34 + a.M44 * b.M44;

            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="s"></param>
        /// <returns></returns>
        public static Matrix4 operator *(Matrix4 a, float s)
        {
            return
            (
                new Matrix4
                (
                    a.M11 * s, a.M12 * s, a.M13 * s, a.M14 * s,
                    a.M21 * s, a.M22 * s, a.M23 * s, a.M24 * s,
                    a.M31 * s, a.M32 * s, a.M33 * s, a.M34 * s,
                    a.M41 * s, a.M42 * s, a.M43 * s, a.M44 * s
                )
            );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static Matrix4 operator /(Matrix4 a, Matrix4 b)
        {
            Matrix4 result;

            // First row
            result.M11 = a.M11 / b.M11 + a.M12 / b.M21 + a.M13 / b.M31 + a.M14 / b.M41;
            result.M12 = a.M11 / b.M12 + a.M12 / b.M22 + a.M13 / b.M32 + a.M14 / b.M42;
            result.M13 = a.M11 / b.M13 + a.M12 / b.M23 + a.M13 / b.M33 + a.M14 / b.M43;
            result.M14 = a.M11 / b.M14 + a.M12 / b.M24 + a.M13 / b.M34 + a.M14 / b.M44;

            // Second row
            result.M21 = a.M21 / b.M11 + a.M22 / b.M21 + a.M23 / b.M31 + a.M24 / b.M41;
            result.M22 = a.M21 / b.M12 + a.M22 / b.M22 + a.M23 / b.M32 + a.M24 / b.M42;
            result.M23 = a.M21 / b.M13 + a.M22 / b.M23 + a.M23 / b.M33 + a.M24 / b.M43;
            result.M24 = a.M21 / b.M14 + a.M22 / b.M24 + a.M23 / b.M34 + a.M24 / b.M44;

            // Third row
            result.M31 = a.M31 / b.M11 + a.M32 / b.M21 + a.M33 / b.M31 + a.M34 / b.M41;
            result.M32 = a.M31 / b.M12 + a.M32 / b.M22 + a.M33 / b.M32 + a.M34 / b.M42;
            result.M33 = a.M31 / b.M13 + a.M32 / b.M23 + a.M33 / b.M33 + a.M34 / b.M43;
            result.M34 = a.M31 / b.M14 + a.M32 / b.M24 + a.M33 / b.M34 + a.M34 / b.M44;

            // Fourth row
            result.M41 = a.M41 / b.M11 + a.M42 / b.M21 + a.M43 / b.M31 + a.M44 / b.M41;
            result.M42 = a.M41 / b.M12 + a.M42 / b.M22 + a.M43 / b.M32 + a.M44 / b.M42;
            result.M43 = a.M41 / b.M13 + a.M42 / b.M23 + a.M43 / b.M33 + a.M44 / b.M43;
            result.M44 = a.M41 / b.M14 + a.M42 / b.M24 + a.M43 / b.M34 + a.M44 / b.M44;

            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="s"></param>
        /// <returns></returns>
        public static Matrix4 operator /(Matrix4 a, float s)
        {
            return 
            (
                new Matrix4
                (
                    a.M11 / s, a.M12 / s, a.M13 / s, a.M14 / s,
                    a.M21 / s, a.M22 / s, a.M23 / s, a.M24 / s,
                    a.M31 / s, a.M32 / s, a.M33 / s, a.M34 / s,
                    a.M41 / s, a.M42 / s, a.M43 / s, a.M44 / s
                )
            );
        }

        #endregion

        #region Matrix Transpose

        /// <summary>
        /// 
        /// </summary>
        /// <param name="m1"></param>
        /// <returns></returns>
        public static Matrix4 Transpose(Matrix4 m1)
        {
            return
            (
                new Matrix4
                (
                    m1.M11, m1.M21, m1.M31, m1.M41,
                    m1.M12, m1.M22, m1.M32, m1.M42,
                    m1.M13, m1.M23, m1.M33, m1.M43,
                    m1.M14, m1.M24, m1.M34, m1.M44
                )
            );
        }

        /// <summary>
        /// 
        /// </summary>
        public void Transpose()
        {
            this = Transpose(this);
        }

        #endregion

        #region Matrix Methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="position"></param>
        /// <param name="lookAt"></param>
        /// <param name="up"></param>
        /// <returns></returns>
        public static Matrix4 CreateLookAt(Vector3 position, Vector3 lookAt, Vector3 up)
        {
            Vector3 zaxis = position - lookAt;
            zaxis.Normalize();
            Vector3 xaxis = Vector3.Cross(up, zaxis);
            xaxis.Normalize();
            Vector3 yaxis = Vector3.Cross(zaxis, xaxis);

            return
            (
                new Matrix4
                (
                    xaxis.X, yaxis.X, zaxis.X, 0,
                    xaxis.Y, yaxis.Y, zaxis.Y, 0,
                    xaxis.Z, yaxis.Z, zaxis.Z, 0,
                    -Vector3.Dot(xaxis, position), -Vector3.Dot(yaxis, position), -Vector3.Dot(zaxis, position), 1
                )
            );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fov"></param>
        /// <param name="aspectRatio"></param>
        /// <param name="nearZ"></param>
        /// <param name="farZ"></param>
        /// <returns></returns>
        public static Matrix4 CreatePerspectiveFieldOfView(float fov, float aspectRatio, float nearZ, float farZ)
        {
            float yscale = 1 / (float)System.Math.Tan(fov / 2f);
            float xscale = yscale / aspectRatio;

            return
            (
                new Matrix4
                (
                    xscale, 0, 0, 0,
                    0, yscale, 0, 0,
                    0, 0, farZ / (nearZ - farZ), -1,
                    0, 0, nearZ * farZ / (nearZ - farZ), 0
                )
            );
        }

        #endregion

        #region Reinforcement and Negation

        /// <summary>
        /// 
        /// </summary>
        /// <param name="m1"></param>
        /// <returns></returns>
        public static Matrix4 operator +(Matrix4 m1)
        {
            return
            (
                new Matrix4
                (
                    +m1.M11, +m1.M11, +m1.M11, +m1.M11,
                    +m1.M11, +m1.M11, +m1.M11, +m1.M11,
                    +m1.M11, +m1.M11, +m1.M11, +m1.M11,
                    +m1.M11, +m1.M11, +m1.M11, +m1.M11
                )
            );
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="m1"></param>
        /// <returns></returns>
        public static Matrix4 operator -(Matrix4 m1)
        {
            return
            (
                new Matrix4
                (
                    -m1.M11, -m1.M11, -m1.M11, -m1.M11,
                    -m1.M11, -m1.M11, -m1.M11, -m1.M11,
                    -m1.M11, -m1.M11, -m1.M11, -m1.M11,
                    -m1.M11, -m1.M11, -m1.M11, -m1.M11
                )
            );
        }

        #endregion

        #region Matrix Comparison

        /// <summary>
        /// 
        /// </summary>
        /// <param name="m1"></param>
        /// <param name="m2"></param>
        /// <returns></returns>
        public static bool operator ==(Matrix4 m1, Matrix4 m2)
        {
            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="m1"></param>
        /// <param name="m2"></param>
        /// <returns></returns>
        public static bool operator !=(Matrix4 m1, Matrix4 m2)
        {
            return !(m1 == m2);
        }

        #endregion

        #region IEquatable Implementation

        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            return obj is Matrix4 && this == (Matrix4)obj;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="m1"></param>
        /// <returns></returns>
        public bool Equals(Matrix4 m1)
        {
            return
            (
                M11 == m1.M11 && M12 == m1.M12 && M13 == m1.M13 && M14 == m1.M14 &&
                M21 == m1.M21 && M22 == m1.M22 && M23 == m1.M23 && M24 == m1.M24 &&
                M31 == m1.M31 && M32 == m1.M32 && M33 == m1.M33 && M34 == m1.M34 &&
                M41 == m1.M41 && M42 == m1.M42 && M43 == m1.M43 && M44 == m1.M44 
            );
        }

        #endregion

        #region Hash Code

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return 
            (
                (int)(M11 + M12 + M13 + M14 +
                      M21 + M22 + M23 + M24 +
                      M31 + M32 + M33 + M34 +
                      M41 + M42 + M43 + M44) / Int32.MaxValue
            );
        }

        #endregion
    }
}
